/*
    Licensed to the Apache Software Foundation (ASF) under one
    or more contributor license agreements.  See the NOTICE file
    distributed with this work for additional information
    regarding copyright ownership.  The ASF licenses this file
    to you under the Apache License, Version 2.0 (the
    "License"); you may not use this file except in compliance
    with the License.  You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing,
    software distributed under the License is distributed on an
    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    KIND, either express or implied.  See the License for the
    specific language governing permissions and limitations
    under the License.
 */
package erwiki.part.attachments.internal.services;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtensionPoint;
import org.eclipse.core.runtime.IExtensionRegistry;
import org.eclipse.core.runtime.Platform;
import org.elwiki_data.AttachmentContent;
import org.elwiki_data.PageAttachment;
import org.elwiki_data.WikiPage;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.ServiceScope;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventHandler;

import erwiki.api.WikiPartReference;
import erwiki.api.attachment.AttachmentManager;
import erwiki.api.attachment.IDynamicAttachment;
import erwiki.api.component.WikiComponent;
import erwiki.api.core.Engine;
import erwiki.api.core.WikiContext;
import erwiki.api.exceptions.ProviderException;
import erwiki.api.exceptions.WikiException;
import erwiki.api.pages.PageManager;
import erwiki.api.preferences.Preferences;
import erwiki.api.providers.AttachmentProvider;
import erwiki.api.rwc.RWC;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;

/**
 * Default implementation for {@link AttachmentManager}
 *
 * {@inheritDoc}
 */
//@formatter:off
@Component(
	name = "erwiki.AttachmentManager",
	service = { AttachmentManager.class, WikiComponent.class, EventHandler.class },
	scope = ServiceScope.SINGLETON)
//@formatter:on
public class DefaultAttachmentManager implements AttachmentManager, WikiComponent, EventHandler {

	/** List of attachment types which are forced to be downloaded */
	@Deprecated
	private String[] m_forceDownloadPatterns;

	private static final Logger log = Logger.getLogger(DefaultAttachmentManager.class);

	private static final String ID_EXTENSION_ATTACHMENT_PROVIDER = "attachmentProvider";

	private Map<String, Class<? extends AttachmentProvider>> attachmentProvidersClasses = new HashMap<>();
	private AttachmentProvider attachmentProvider;

	@Deprecated
	private CacheManager m_cacheManager = CacheManager.getInstance();
	@Deprecated
	private Cache m_dynamicAttachments;

	/**
	 * Creates instance of DefaultAttachmentManager.
	 */
	public DefaultAttachmentManager() {
		super();
	}

	// -- OSGi service handling ----------------------(start)--

	@WikiPartReference
	private Engine m_engine;

	@WikiPartReference
	private PageManager pageManager;

	/** {@inheritDoc} */
	// FIXME: Perhaps this should fail somehow.
	@Override
	public void initialize() throws WikiException {
		loadAttachmentProviders();
		attachmentProvider = instatntiateAttachmentProvider(
				getPreference(AttachmentManager.Prefs.ATTACHMENT_PROVIDER, String.class));

		m_forceDownloadPatterns = propsForceDownload();
	}

	/**
	 * Создает экземпляр AttachmentProvider, по заданному идентификатору провайдера.
	 *
	 * @param requiredId Идентификатор требуемого провайдера присоединений.
	 * @return Может вернуть <code>null</code>, если что-то пойдет не так, как ожидалось.
	 */
	private AttachmentProvider instatntiateAttachmentProvider(String requiredId) {
		Class<? extends AttachmentProvider> clazz = attachmentProvidersClasses.get(requiredId);
		AttachmentProvider provider = null;
		try {
			provider = clazz.getDeclaredConstructor().newInstance();
			m_engine.initialiseReferencedFields(provider); // Reflection, аннотации...
			provider.initialize(m_engine);
		} catch (Exception e) {
			log.fatal("Attachment provider " + requiredId + " cannot be instantiated.", e);
		}

		return provider;
	}

	// -- OSGi service handling ------------------------(end)--

	/**
	 * Загружает из расширений "attachmentProvider" классы, реализующие AttachmentProvider.
	 */
	private void loadAttachmentProviders() {
		String namespace = FrameworkUtil.getBundle(this.getClass()).getSymbolicName();
		IExtensionRegistry registry = Platform.getExtensionRegistry();
		IExtensionPoint ep = registry.getExtensionPoint(namespace, ID_EXTENSION_ATTACHMENT_PROVIDER);
		if (ep != null) {
			for (IConfigurationElement el : ep.getConfigurationElements()) {
				String contributorName = el.getContributor().getName();
				String className = el.getAttribute("class");
				String attachmentProviderId = el.getAttribute("id");

				try {
					Bundle bundle = Platform.getBundle(contributorName);
					Class<?> clazz = bundle.loadClass(className);
					try {
						Class<? extends AttachmentProvider> cl = clazz.asSubclass(AttachmentProvider.class);
						this.attachmentProvidersClasses.put(attachmentProviderId, cl);
					} catch (ClassCastException e) {
						log.fatal("Attachment Provider " + className + " is not extends AttachmentProvider interface.");
					}
				} catch (ClassNotFoundException e) {
					log.fatal("Attachment Provider " + className + " cannot be found.");
				}
			}
		}
	}

	/** {@inheritDoc} */
	@Override
	public boolean attachmentsEnabled() {
		return getCurrentProvider() != null;
	}

	@Override
	public int propsMaxAllowedUploadSize() {
		return getPreference(AttachmentManager.Prefs.UPLOAD_MAXSIZE, Integer.class);
	}

	protected String[] propsAllowedExtensions() {
		String extensions = getPreference(AttachmentManager.Prefs.ALLOWED_EXTENSIONS, String.class);
		return (StringUtils.isNotBlank(extensions)) ? extensions.toLowerCase().split("\\s") : new String[0];
	}

	protected String[] propsForbiddenExtensions() {
		String extensions = getPreference(AttachmentManager.Prefs.FORBIDDEN_EXTENSIONS, String.class);
		return (StringUtils.isNotBlank(extensions)) ? extensions.toLowerCase().split("\\s") : new String[0];
	}

	protected String[] propsForceDownload() {
		String extensions = getPreference(AttachmentManager.Prefs.FORCE_DOWNLOAD, String.class);
		return (StringUtils.isNotBlank(extensions)) ? extensions.toLowerCase().split("\\s") : new String[0];
	}

	@Override
	public boolean isFileTypeAllowed(String extension) {
		if (StringUtils.isBlank(extension)) {
			return false;
		}

		extension = extension.toLowerCase();

		if (Arrays.asList(propsForbiddenExtensions()).contains(extension)) {
			return false;
		}

		String[] allowedExtensions = propsAllowedExtensions();
		if (Arrays.asList(allowedExtensions).contains(extension)) {
			return true;
		}

		return allowedExtensions.length == 0;
	}

	/** {@inheritDoc} */
	@Override
	public String getAttachmentId(String attachmentName) {
		AttachmentContent atc;
		try {
			atc = getAttachmentContent(attachmentName);
		} catch (final ProviderException e) {
			log.warn("Finding attachments failed: ", e);
			return null;
		}

		if (atc != null) {
			return atc.getPageAttachment().getId();
		}

		return null;
	}

	/** {@inheritDoc} */
	@Override
	public String getAttachmentName(final WikiContext context, final String attachmentname) {
		final AttachmentContent att;
		try {
			att = getAttachmentContent(attachmentname);
		} catch (final ProviderException e) {
			log.warn("Finding attachments failed: ", e);
			return null;
		}

		if (att != null) {
			return att.getPageAttachment().getName();
		} else if (attachmentname.indexOf('/') != -1) {
			return attachmentname;
		}

		return null;
	}

	@Override
	public AttachmentContent getAttachmentContentById(String atchId, int... version) throws ProviderException {
		AttachmentContent attachmentInfo = getCurrentProvider().getAttachmentContentById(atchId, version);

		return attachmentInfo;
	}

	/**
	 * {@inheritDoc}
	 *
	 * @throws ProviderException
	 */
	@Override
	public AttachmentContent getAttachmentContent(WikiPage wikiPage, String attachmentName, int... version)
			throws ProviderException {
		AttachmentContent attachmentInfo = getCurrentProvider().getAttachmentContent(wikiPage, attachmentName, version);

		return attachmentInfo;
	}

	/** {@inheritDoc} */
	@Override
	public AttachmentContent getAttachmentContent(String attachment, int version) throws ProviderException {
		if (getCurrentProvider() == null) {
			return null;
		}

		WikiPage pageOfAttachment = null;
		String attachmentName = null;

		// Figure out the parent page of this attachment. If we can't find it, we'll assume this refers
		// directly to the attachment.
		String[] params = attachment.split("/");
		if (params.length == 1) {
			// attachment is from current page.
			pageOfAttachment = RWC.INSTANCE.getPage();
			attachmentName = params[0].trim();
		} else if (params.length == 2) {
			// attachment is from desired page.
			String pageId = params[0].trim().substring(1);
			pageOfAttachment = pageManager.getPageById(pageId);
			attachmentName = params[1].trim();
		} else {
			// incorrect attachment required name.
			return null;
		}

		// If the page cannot be determined, we cannot possibly find the attachments.
		if (pageOfAttachment == null) {
			return null;
		}

		AttachmentContent attachmentContent = getCurrentProvider().getAttachmentContent(pageOfAttachment,
				attachmentName, version);

		return attachmentContent;
	}

	/** {@inheritDoc} */
	@Override
	public List<PageAttachment> listAttachments(final WikiPage wikipage) throws ProviderException {
		if (getCurrentProvider() == null) {
			return new ArrayList<>();
		}

		final List<PageAttachment> atts = new ArrayList<>(getCurrentProvider().listAttachments(wikipage));
		atts.sort(Comparator.comparing(PageAttachment::getName, this.pageManager.getPageSorter()));

		return atts;
	}

	/** {@inheritDoc} */
	@Override
	public boolean forceDownload(String name) {
		if (name == null || name.length() == 0) {
			return false;
		}

		name = name.toLowerCase();
		if (name.indexOf('.') == -1) {
			return true; // force download on attachments without extension or type indication
		}

		for (final String forceDownloadPattern : m_forceDownloadPatterns) {
			if (name.endsWith(forceDownloadPattern) && forceDownloadPattern.length() > 0) {
				return true;
			}
		}

		return false;
	}

	/** {@inheritDoc} */
	@Override
	public InputStream getAttachmentStream(AttachmentContent atchContent) throws ProviderException, IOException {
		if (getCurrentProvider() == null) {
			return null;
		}

		return getCurrentProvider().getAttachmentData(atchContent);
	}

	/** {@inheritDoc} */
	@Override
	public void storeDynamicAttachment(final WikiContext ctx, final IDynamicAttachment att) {
		m_dynamicAttachments.put(new Element(att.getName(), att));
	}

	/** {@inheritDoc} */
	/*:FVK:
	@Override
	public DynamicAttachment getDynamicAttachment( final String name ) {
	    final Element element = m_dynamicAttachments.get( name );
	    if( element != null ) {
	        return ( DynamicAttachment )element.getObjectValue();
	    } else {
	        // Remove from cache, it has expired.
	        m_dynamicAttachments.put( new Element( name, null ) );
	        return null;
	    }
	}
	*/

	@Override
	public void storeAttachment(WikiPage wikiPage, AttachmentContent attContent, String attName, InputStream data)
			throws IOException, ProviderException {
		if (getCurrentProvider() == null) {
			return;
		}

		// Checks if the actual, real page exists without any modifications or aliases.
		// We cannot store an attachment to a non-existent page.
		if (wikiPage == null || !pageManager.pageExists(wikiPage)) {
			String msg = Preferences.getBundle().getString("attach.parent.not.exist");
			throw new ProviderException(msg);
		}

		getCurrentProvider().putAttachmentData(wikiPage, attContent, attName, data);
	}

	/** {@inheritDoc} */
	@Override
	public List<AttachmentContent> getVersionHistory(final String attachmentName) throws ProviderException {
		if (getCurrentProvider() == null) {
			return null;
		}

		final AttachmentContent att = getAttachmentContent(attachmentName);
		if (att != null) {
			return getCurrentProvider().getVersionHistory(att.getPageAttachment());
		}

		return null;
	}

	/** {@inheritDoc} */
	@Override
	public Collection<PageAttachment> getAllAttachments() throws ProviderException {
		if (attachmentsEnabled()) {
			return getCurrentProvider().listAllChanged(new Date(0L));
		}

		return new ArrayList<>();
	}

	/** {@inheritDoc} */
	@Override
	public AttachmentProvider getCurrentProvider() {
		return attachmentProvider;
	}

	/** {@inheritDoc} */
	@Override
	public void deleteVersion(final PageAttachment att) throws ProviderException {
		if (getCurrentProvider() == null) {
			return;
		}

		getCurrentProvider().deleteVersion(att);
	}

	/** {@inheritDoc} */
	@Override
	public void deleteAttachmentContent(AttachmentContent att) throws ProviderException {
		if (getCurrentProvider() == null) {
			return;
		}
		getCurrentProvider().deleteAttachmentContent(att);
	}

	/** {@inheritDoc} */
	@Override
	public void deleteAttachmentById(String attachmentId) throws ProviderException {
		if (getCurrentProvider() == null) {
			return;
		}

		AttachmentProvider atchProvider = getCurrentProvider();
		atchProvider.deleteAttachmentById(attachmentId);
	}

	@Override
	public void deleteAttachment(PageAttachment pageAttachment) throws ProviderException {
		if (getCurrentProvider() == null) {
			return;
		}

		AttachmentProvider atchProvider = getCurrentProvider();
		atchProvider.deleteAttachment(pageAttachment);
	}

	@Override
	public void handleEvent(Event event) {
		/*String topic = event.getTopic();
		switch (topic) {
			break;
		}*/
	}

}
